/**
*    Copyright (C) 2008  Chase Kernan 
*    chase.kernan@gmail.com
*
*    This program is free software: you can redistribute it and/or modify
*    it under the terms of the GNU General Public License as published by
*    the Free Software Foundation, either version 3 of the License, or
*    (at your option) any later version.
*
*    This program is distributed in the hope that it will be useful,
*    but WITHOUT ANY WARRANTY; without even the implied warranty of
*    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
*    GNU General Public License for more details.
*
*    You should have received a copy of the GNU General Public License
*    along with this program.  If not, see <http://www.gnu.org/licenses/>.
* 
*/

package com.chasekernan.hxnova.core.fleets;

import com.chasekernan.hxnova.core.dataholders.Global;
import com.chasekernan.hxnova.core.players.Player;
import com.chasekernan.hxnova.core.components.Engine;
import com.chasekernan.hxnova.core.designs.HullDesign;
import com.chasekernan.hxnova.core.minerals.MineralSet;
import com.chasekernan.hxnova.core.races.ResearchCosts;
import com.chasekernan.hxnova.core.races.TechLevel;
import com.chasekernan.hxnova.core.SpaceObject;
import com.chasekernan.hxnova.core.stars.Concentrations;
import com.chasekernan.hxnova.utils.Utils;
import com.chasekernan.hxnova.utils.Vector;
import com.chasekernan.hxnova.utils.XmlBuilder;
import com.chasekernan.hxnova.core.stars.Star;
import com.chasekernan.hxnova.core.battles.BattlePlan;


//TODO: Very important -- change armor and shields from ints to floats.

/**
    These vars like shield and armor are distributed based on stack,
    others like cargo are collected with the fleet as a hole.
**/
typedef ShipStack = {
    var amount : Null<Int>;
    var type : HullDesign;
    var armor : Null<Int>;
    var shields : Null<Int>;
    var battlePlan : BattlePlan;
}

//----------------------------------------------------------------------
//WAYPOINT MANAGEMENT
//----------------------------------------------------------------------

/**
    The end destination of a waypoint.
**/
enum SpaceTarget {
    Location(v : Vector);
    FleetTarget(f : Fleet);
    StarTarget(s : Star);
}

/**
    Describes where the loading/unloading is going to occur.
**/
enum CargoTarget {
    StarCargo(s : Star);
    FleetCargo(f : Fleet);
    SalvageCargo(s : Salvage);
}

/**
    The task performed by the fleet upon reaching the destination.
    NOTE: A mineral set isn't used for drop/unload because each mineral type
    can have a [CargoAmount].
**/
enum Task {
    NoTask;
    Colonization;
    Drop(fuel : CargoAmount, minerals : Hash<CargoAmount>, 
            colonists : CargoAmount, target : CargoTarget);
    Load(fuel : CargoAmount, minerals : Hash<CargoAmount>, 
            colonists : CargoAmount, target : CargoTarget);
    MergeWithFleet(f : Fleet);
    ScrapFleet;
    TransferFleet(newOwner : Player);
}

/**
    The amount of cargo to be loaded or unloaded.
**/
enum CargoAmount {
    All;
    None;
    Value(v : Int);
    Percent(p : Float);
}

/**
    Fleets have a list of waypoints.
**/
typedef Waypoint = {
    var warp : Null<Int>;
    var task : Task;
    var target : SpaceTarget;
}

//----------------------------------------------------------------------

/**
    Contains stacks of ships of the same kind as well as cargo and fuel.
    Moves in space and wages battles.
**/
class Fleet extends SpaceObject {
    
    public static var CONSUMPTION_RATE : Float = 200.0;
    public static var COLONISTS_PER_KT = 100;
    
    public var name : String;
    public var owner : Player;
    public var ships : Array<ShipStack>;
    public var fuel : Null<Float>;
    public var colonists : Null<Int>;
    public var minerals : MineralSet;
    public var bearing : Null<Float>;
    public var scannedMass : Null<Int>;
    public var scannedWarp : Null<Int>;
    
    public var waypoint0 : Task;
    public var waypoints : Array<Waypoint>;
    
    /**
        Sets all variables to null.
    **/
    public function new() {
        super();
    }
    
    /**
        Returns how much fuel the fleet can hold.
    **/
    public function getFuelCapacity() : Float {
        var cap = 0.0;
        for (s in ships) cap += s.amount * s.type.getFuelCapacity();
        return cap;
    }
    
    /**
        Returns how much cargo the fleet can hold.
    **/
    public function getCargoCapacity() : Int {
        var cap : Int = 0;
        for (s in ships) cap += s.amount & s.type.getCargoCapacity();
                return cap;
    }
    
    /**
        Merges this fleet with another.
        Will throw an error if the Fleet owners don't match.
        NOTE: When called like fleet1.mergeFleet(fleet2), the battle plans from
        fleet 1 are used instead of those in fleet2.
    **/
    public function mergeFleet(fleet : Fleet) {
        if (fleet.owner.id != owner.id) throw "Fleet owners much match.";
        
        for (s1 in fleet.ships) {
            for (s2 in ships) {
                if (s1.type == s2.type) {
                    s2.amount += s1.amount;
                    s2.armor += s1.armor;
                    s2.shields += s1.shields;
                    break;
                }
            }
        }
        
        fuel += fleet.fuel;
        minerals.add(fleet.minerals);
        colonists += fleet.colonists;
    }
    
    /**
        Returns the mass of the entire fleet including all of the cargo weight.
    **/
    public function getMass() : Int {
        var mass = minerals.getMass() + 
                   Math.round(colonists / COLONISTS_PER_KT);
        
        for (shipSet in ships) {
            mass += shipSet.amount * shipSet.type.getTotalMass();
        }
        
        return mass;
    }
    
    /**
        Adds new ships to the stacks. If there is a stack with the same kind of
        ships, then these are just added to that stack, otherwise, a new stack
        is created.
        Will throw an error if the amount is less than 1, if the type == null, 
        or the design is a starbase.
    **/
    public function addNewShips(type : HullDesign, amount : Int) {
        if (amount <= 0) throw "Amount of ships must be greater than 0.";
        if (type == null) throw "Type must not be null...";
        if (type.isStarbase()) throw "Cannot add starbases to a fleet.";
        
        if (ships == null) ships = new Array();
        //check if there is already ships of that type in the fleet
        var flag = false;
        for (s in ships) {
            if (s.type == type) {
                s.amount += amount;
                s.armor += type.getArmor() * amount;
                s.shields += type.getShields() * amount;
                flag = true;
                break;
            }
        }
        
        if (!flag) ships.push( { type : type, amount : amount, 
                armor : (type.getArmor() * amount), 
                shields : (type.getShields() * amount),
                battlePlan : owner.defaultBattlePlan} );
        
        if (fuel == null) fuel = 0;
        fuel += type.getFuelCapacity() * amount;
    }
    
    /**
        Removes a set of ships from the fleet and returns hte fuel, colonists, 
        and minerals they were carrying.
        Will throw an error if amount is less than 1, the type is null, the
        amount is greater than the number of ships of that type in the fleet, or
        if there is no such type of ships in the fleet.
    **/
    public function removeShips(type : HullDesign, amount : Int) : 
            {fuel : Float, colonists : Int, minerals : MineralSet} {
                
        if (amount <= 0) throw "Amount of ships must be greater than 0.";
        if (type == null) throw "Type cannot be null.";
        
        for (i in 0...ships.length) {
            var s = ships[i];
            
            if (s.type == type) {
                if (s.amount < amount) throw "Tried to remove more ships " + 
                        "than possible.";
                
                var previousAmount = s.amount;
                var currentAmount = if (s.amount == amount) 0 
                                    else s.amount - amount;
                
                var diff = previousAmount - currentAmount;
                var fuelCarried = fuel * (diff * s.type.getFuelCapacity()) 
                                         / getFuelCapacity();
                                         
                var totalCargoCapacity = getCargoCapacity();
                //used an [if] here to avoid a zero divide
                var cargoPercent = if (totalCargoCapacity == 0) 0 
                                   else (diff * s.type.getCargoCapacity()) 
                                        / totalCargoCapacity;
                
                var colonistsCarried = Std.int(colonists * cargoPercent);
                var mineralsCarried = minerals.clone().multiply(cargoPercent);
                
                if (s.amount == amount) ships.splice(i, 1);
                else {
                    s.amount -= amount;
                    currentAmount = s.amount;
                    s.armor -= amount * type.getArmor();
                    s.shields -= amount * type.getShields();
                }
                
                return { fuel : fuelCarried, colonists : colonistsCarried, 
                         minerals : mineralsCarried };
            }
        }
        
        throw "No such type of ships in this fleet.";
    }
    
    /**
        Returns the amount of fuel used to travel a certain distance at the 
        given warp.
        Will throw an error if the distance is <= 0 or the warp is out of range.
    **/
    public function getFuelUsed(distance : Float, warp : Int) : Float {
        if (distance <= 0) throw "Distance must be greater than 0.";
        if (warp < Engine.WARP_MIN || warp > Engine.WARP_MAX) {
            throw "Warp is out of range.";
        }
        
        var time = distance / (warp * warp);
        
        //sort the array based on the best efficiency engines
        //so the most cargo is put on the most efficient ships.
        var copy = ships.copy();
        var ordered  = new Array<ShipStack>();
        
        while (copy.length != 0) {
            var bestIndex = 0;
            var best = copy[0].type.getEngineType().getEfficiency(warp);
            for (i in 1...copy.length) {
                var e = copy[i].type.getEngineType().getEfficiency(warp);
                if (e <= best) {
                    bestIndex = i;
                    best = e;
                }
            }
            
            ordered.push(copy[bestIndex]);
            copy.splice(bestIndex, 1);
        }
        
        if (colonists == null) colonists = 0;
        if (minerals == null) minerals = new MineralSet();
        var totalCargo = colonists + minerals.getMass();
        var fuelUsed = 0.0;
        
        for (s in ordered) {
            var c = Std.int(Math.min(s.type.getCargoCapacity(), 
                                     totalCargo));
            //time x amount x (weight x (efficiency / 100) x warp x warp) / RATE
            fuelUsed += time * s.amount * ((c + s.type.getTotalMass()) * 
                        s.type.getEngineType().getEfficiency(warp) 
                        / 100 * warp * warp) / CONSUMPTION_RATE;
            totalCargo -= c;
        }
        
        return fuelUsed;
    }
    
        /**
        Returns true if the fleet is stopped.
        TODO: Allow engines to travel at warp 0.
    **/
    public function isStopped() : Bool {
        return waypoints.length == 0;
    }
    
    /**
        Returns the resources/minerals used in creating the fleet (used for 
        scrapping).
    **/
    public function getTotalCost() : 
            { resources : Int, minerals : MineralSet } {
        var resources = 0;
        var minerals = new MineralSet();
        
        for (stack in ships) {
            var typeCost = stack.type.getTotalCost();
            resources += stack.amount * typeCost.resourceCost;
            minerals.add(typeCost.mineralCost.multiply(stack.amount));
        }
        
        return { resources : resources, minerals : minerals };
    }
    
    /**
        Returns the range of the fleet's normal scanners.
    **/
    public function getNormalScanningRange() : Int {
        var largestRange = 0;
        
        for (stack in ships) {
            var stackRange = stack.type.getNormalScanningRange();
            if (stackRange > largestRange) largestRange = stackRange;
        }
        
        return largestRange;
    }
    
    /**
        Returns the range of the fleet's penetrating scanners.
    **/
    public function getPenetratingScanningRange() : Int {
        var largestRange = 0;
        
        for (stack in ships) {
            var stackRange = stack.type.getPenetratingScanningRange();
            if (stackRange > largestRange) largestRange = stackRange;
        }
        
        return largestRange;
    }
    
    /**
        Returns true if the data that the server should know (id, fuel, ships,
        etc...) is defined in the fleet.
        
        NOTE: This also checks that the fuel, colonists and etc are not below 0.
    **/
    public function checkForCompleteServerInfo() : Bool {
        return !(id == null || name == null || 
                 owner == null || 
                 ships == null || ships.length == 0 || 
                 colonists == null || colonists < 0 || 
                 fuel == null || fuel < 0 || 
                 minerals == null || waypoint0 == null ||
                 waypoints == null);
    }
    
    /**
        Returns true if the fleet has a scanner in one of ships.
        NOTE: This is used so that range 0 scanners (ie bat scanner)
        can be detected and used by the scanner functions.
    **/
    public function hasScanner() : Bool {
        for (stack in ships) {
            if (stack.type.hasScanner()) return true;
        }
        
        return false;
    }
    
    /**
        Returns the names of the fields of tech where this design is higher in
        level than the level provided.
    **/
    public function getAdditionalTech(level : TechLevel) : Array < String > {
        var fields = new Array<String>();
        
        //go through each type of ship, each component, and each hull and check
        //for new tech fields.
        
        for (stack in ships) {
            var newFields = stack.type.baseHull.requiredTech
                    .getAdditionalTech(level);
            
            for (designPart in stack.type.slots) {               
                newFields.concat(designPart.component.requiredTech
                        .getAdditionalTech(level));     
            }
            
            for (field in newFields) {
                if (!arrayContains(fields, field)) fields.push(field);
            }
        }
        
        return fields;
    }
    
    /**
        Returns the direction of the fleet.
        Will return -1 if the fleet is stopped.
        TODO: Allow this to be saved for the client.
    **/
    public function getBearing() : Float{
        if (isStopped()) return -1;
        
        var targetLocation = switch(waypoints[0].target) {
            
            case Location(v):
                v;
                
            case FleetTarget(f):
                Global.fleets.getById(f.id, f.owner.id).location.clone();
                
            case StarTarget(s):
                Global.map.stars[s.id].location.clone();
        };
        
        return targetLocation.getSub(location).getAngle();
    }

    
    /**
        Moves the fleet towards the given target at the given warp.
        Will only move a certain percentage of its total capability if percent
        amount is specified.
    **/
    public function move(target : Vector, warp : Int, ?percentAmount : Float) {
        if (percentAmount != null) {
            target = location.getAdd(
                    target.getSub(location).getScale(percentAmount));
        }
        
        var fuelUsage = 0.0;
        var travelled = true;
        var distanceAble = warp * warp;
        
        if(location.distanceTo(target) > distanceAble) {
            travelled = false;
            var p = distanceAble / target.distanceTo(location);
            var newx = p * (target.x - location.x);
            var newy = p * (target.y - location.y);
            target = location.getAdd(new Vector(newx, newy));
        }
        
        var distance = target.distanceTo(location);
        
        fuelUsage = getFuelUsed(distance, warp);
        
        if (fuelUsage <= fuel) {
            fuel -= fuelUsage;
            location = target.clone();
        } else {
            var percent : Float = fuel / fuelUsage;
            var xdist = percent * (target.x - location.x);
            var ydist = percent * (target.y - location.y);
            location = location.getAdd(new Vector(xdist, ydist));
            
            //TODO: Add message
            var largestFreeSpeed = Engine.WARP_MAX;
            for (ship in ships) {
                var current = ship.type.getEngineType()
                              .getHighestFreeTravelWarp();
                if (current < largestFreeSpeed) largestFreeSpeed = current;
            }
            
            waypoints[0].warp = largestFreeSpeed;
            fuel = 0.0;
        }
    }
    
    public static var XML_TITLE = "fleet";
    
    /**
        Writes a fleet to an xml file.
        Smaple xml:
            TODO: run [writeToXml]
    **/
    public static function writeToXml(fleet : Fleet) : Xml {
        var xml : XmlBuilder = new XmlBuilder(XML_TITLE);
        
        xml.addAttributes(fleet, ["id", "name", "owner", "fuel", "scannedMass", 
                                  "scannedWarp", "bearing"]);
        xml.addAttributes(fleet.location, ["x", "y"]);
        
        if (fleet.ships != null) {
            var shipsXml = new XmlBuilder("ships");
            
            for (ship in fleet.ships) {
                var shipXml = new XmlBuilder("ship");
                shipXml.addAttributes(ship, 
                        ["amount", "type", "armor", "shields"]);
                shipXml.addAttribute("battle_plan", ship.battlePlan.name);
                
                shipsXml.addChild(shipXml); 
            }
            xml.addChild(shipsXml);
        }
        
        if (fleet.waypoint0 != null) {
            var wp0 = new XmlBuilder("waypoint0");
            writeTask(wp0, fleet.waypoint0);
            xml.addChild(wp0);
        }
        
        
        if (fleet.waypoints != null) {
            var wps = new XmlBuilder("waypoints");
            
            for (wp in fleet.waypoints) {
                var wpXml = new XmlBuilder("waypoint");
                wpXml.addAttribute("warp", wp.warp);
                
                writeTask(wpXml, wp.task);
                
                wpXml.addAttribute("target", switch(wp.target) {
                    
                    case Location(v):
                        wpXml.addAttribute("target_x", v.x);
                        wpXml.addAttribute("target_y", v.y);
                        "location";
                        
                    case StarTarget(s):
                        wpXml.addAttribute("star_id", s.id);
                        "star";
                        
                    case FleetTarget(f):
                        wpXml.addAttribute("fleet_id", f.id);
                        wpXml.addAttribute("fleet_owner", f.owner.id);
                        "fleet";
                });
                wps.addChild(wpXml);
            }
            xml.addChild(wps);
        }
        
        return xml.xml;
    }
    
    private static function writeCargoTarget(xml : Xml, ct : CargoTarget) {
        xml.set('cargo_target', switch(ct) {
            
            case StarCargo(s):
                xml.set('star_id', Std.string(s.id));
                'star';
                
            case FleetCargo(f):
                xml.set("fleet_owner", Std.string(f.owner.id));
                xml.set('fleet_id', Std.string(f.id));
                'fleet';
                
            case SalvageCargo(s):
                xml.set('salvage_id', Std.string(s.id));
                'salvage';
        });
    }
    
    private static function writeTask(xml : XmlBuilder, task : Task) {
        xml.addAttribute("task", switch(task) {
                
            case NoTask:
                "no_task";
                    
            case Colonization:
                "colonization";
                    
            case Drop(fuel, minerals, colonists, ct):
                xml.addAttribute("fuel", writeCargoAmount(fuel));
                for (minType in Concentrations.MINERAL_TYPES) {
                    xml.addAttribute(minType, minerals.get(minType));
                }
                xml.addAttribute("colonists", writeCargoAmount(colonists));
                writeCargoTarget(xml.xml, ct);
                "drop";
                    
            case Load(fuel, minerals, colonists, ct):
                xml.addAttribute("fuel", writeCargoAmount(fuel));
                for (minType in Concentrations.MINERAL_TYPES) {
                    xml.addAttribute(minType, minerals.get(minType));
                }
                xml.addAttribute("colonists", writeCargoAmount(colonists));
                writeCargoTarget(xml.xml, ct);
                "load";
                    
            case MergeWithFleet(f):
                xml.addAttribute("merge_fleet", f.id);
                "merge_with_fleet";
                    
            case ScrapFleet:
                "scrap_fleet";
                    
            case TransferFleet(newOwner):
                xml.addAttribute("new_owner", newOwner.id);
                "transfer_fleet";
        });
    }
    
    private static function writeCargoAmount(a : CargoAmount) : String {
        return switch(a) {
            
            case All: "all";
            
            case None : "none";
            
            case Value(v) : "value-" + Std.string(v);
            
            case Percent(p) : "percent-" + Std.string(p);
        };
    }
        
    /**
        Reads a fleet from an xml file.
        Will throw an error if there are any mistakes.
    **/
    public static function readFromXml(xml : Xml) : Fleet {
        try {
            var fleet = new Fleet();
            
            var xb : XmlBuilder = XmlBuilder.fromXml(xml);
            
            fleet.id = xb.readIntAttribute("id");
            
            var ownerId = xb.readIntAttribute("owner");
            if (ownerId != null) 
                fleet.owner = Global.players.getPlayerById(ownerId);
            
            var x = xb.readFloatAttribute("x");
            var y = xb.readFloatAttribute("y");
            if (x != null && y != null) fleet.location = new Vector(x, y);
                
            fleet.fuel = xb.readFloatAttribute("fuel");
            
            fleet.scannedMass = xb.readIntAttribute("scannedMass");
            fleet.scannedWarp = xb.readIntAttribute("scannedWarp");
            fleet.bearing = xb.readFloatAttribute("bearing");
            
            if (Utils.xmlHasElement(xml, "ships")) {
                
                fleet.ships = new Array();
                
                for (shipXml in Utils.getXmlElement(xml, "ships")) {
                    var shipXB : XmlBuilder = XmlBuilder.fromXml(shipXml);
                    
                    var amount = shipXB.readIntAttribute("amount");
                    var armor = shipXB.readIntAttribute("armor");
                    var shields = shipXB.readIntAttribute("shields");
                    var type : HullDesign = null;
                    var battlePlan = fleet.owner.getBattlePlanByName(
                            shipXB.readStringAttribute("battle_plan"));
                    
                    if (amount != null && amount < 1) {
                        throw "Amount can't be less than 1.";
                    }
                    
                    if (shipXml.exists("type")) {
                        if (fleet.owner == null) 
                            throw "Cannot know type of ships if owner is not " +
                                    "known.";
                        type = fleet.owner.getDesignByName(
                            shipXB.readStringAttribute("type"));
                    }
                    
                    if (armor != null && armor < 1) {
                        throw "Armor can't be less than 1.";
                    }
                    
                    if (shields != null && shields < 0) {
                        throw "Shields can't be less than 0.";
                    }
                    
                    fleet.ships.push( { amount : amount, type : type, 
                                        armor : armor, shields: shields,
                                        battlePlan : battlePlan } );
                }
            }
            
            if (Utils.xmlHasElement(xml, "waypoint0")) {
                var waypoint0 : Xml = Utils.getXmlElement(xml, "waypoint0");
                fleet.waypoint0 = readWaypoint0FromXml(waypoint0, fleet.owner);
            }
            
            if(Utils.xmlHasElement(xml, "waypoints")) {
                fleet.waypoints = readWaypointsFromXml(
                        Utils.getXmlElement(xml, "waypoints"), fleet.owner);
            }
            
            return fleet;
        } catch (e : Dynamic) {
            throw "Error while reading from fleet file: " + e;
        }
    }
    
    private static function readCargoAmount(amount : String) : CargoAmount {
        var index = amount.indexOf('-');
        return switch(if(index == -1) amount else amount.substr(0, index + 1)) {
            
            case "all": All;
            
            case "none": None;
            
            case "value":
                var v = Std.parseInt(amount.substr(index + 1));
                if (v < 0) throw "Cargo amount value must be greater than 0.";
                Value(v);
            
            case "percent": 
                var p = Std.parseFloat(amount.substr(index + 1));
                if (p < 0.0 || p > 1.0) {
                    throw "Cargo amount percent must be >= 0 and <= 1.";
                }
                Percent(p);
            
            default: throw "Corrupted cargo amount: " + amount;
        };
    }
    
    
    
    private static function readCargoTarget(xml : Xml) : CargoTarget {
        return switch(xml.get('cargo_target')) {
            
            case 'star':
                StarCargo(Global.stars.getStarById(Std.parseInt(
                        xml.get('star_id'))));
                        
            case 'fleet':
                FleetCargo(Global.fleets.getById(
                        Std.parseInt(xml.get('fleet_id')), 
                        Std.parseInt(xml.get("fleet_owner"))));
                        
            case 'salvage':
                SalvageCargo(Global.salvage.getById(Std.parseInt(
                        xml.get('salvage_id'))));
        };
    }
    
    public static function readWaypointsFromXml(xml : Xml, owner : Player) 
            : Array < Waypoint > {
        var waypoints = new Array<Waypoint>();
        
        for(wp in xml.elementsNamed("waypoint")) {
            var warp = Std.parseInt(wp.get("warp"));
            var task = readWaypoint0FromXml(wp, owner);
                        
            var target : SpaceTarget = switch(wp.get("target")) {
                
                case "location":
                    Location(new Vector(Std.parseFloat(wp.get("x")), 
                            Std.parseFloat(wp.get("y"))));
                            
                case "star":
                    var id = Std.parseInt(wp.get("star_id"));
                    StarTarget(Global.stars.getStarById(id));
                    
                case "fleet":
                    var id = Std.parseInt(wp.get("fleet_id"));
                    var ownerID = Std.parseInt(wp.get("fleet_owner"));
                    FleetTarget(Global.fleets.getById(id, ownerID));
                    
                default:
                    throw "Corrupted target.";
            };
            waypoints.push({warp : warp, task : task, target : target});
        }
        return waypoints;
    }
    
    public static function readWaypoint0FromXml(xml : Xml, owner : Player) 
            : Task {
        return switch(xml.get("task")) {
            
            case "no_task":
                NoTask;
                
            case "colonization":
                Colonization;
                
            case "drop":
                    var fuel = readCargoAmount(xml.get("fuel"));
                    var minerals = new Hash<CargoAmount>();
                    for (minType in Concentrations.MINERAL_TYPES) {
                        minerals.set(minType, 
                            if (!xml.exists(minType)) None
                            else readCargoAmount(xml.get(minType)));
                    }
                    
                    var colonists = readCargoAmount(xml.get("colonists"));
                    var ct = readCargoTarget(xml);
                    Drop(fuel, minerals, colonists, ct);
                    
                case "load":
                    var fuel = readCargoAmount(xml.get("fuel"));
                    var minerals = new Hash<CargoAmount>();
                    for (minType in Concentrations.MINERAL_TYPES) {
                        minerals.set(minType, 
                            if (!xml.exists(minType)) None
                            else readCargoAmount(xml.get(minType)));
                    }
                    
                    var colonists = readCargoAmount(xml.get("colonists"));
                    var ct = readCargoTarget(xml);
                    Load(fuel, minerals, colonists, ct);
                
            case "merge_with_fleet":
                var f = Global.fleets.getById(
                        Std.parseInt(xml.get("merge_fleet")), owner.id);
                MergeWithFleet(f);
                
            case "scrap_fleet":
                ScrapFleet;
                
            case "transfer_fleet":
                TransferFleet(Global.players.getPlayerById(Std.parseInt(
                        xml.get("new_owner"))));
                        
            default:
                throw "Corrupted task.";
        };
    }
    
    private static function arrayContains<T>(array : Array<T>, 
            value : T) : Bool {
        for (v in array) {
            if (v == value) return true;
        }
        return false;
    }
    
    public static function getById(id : Int) : Fleet {
        throw "This function is broken (XmlSerializer is no longer " +
              "supported).";
        return null;
    }
    
    public function clone() : Fleet {
        //TODO: Forgot about Reflect.copy... Should check to see if it gives the
        //same results as the method below.
        var f = new Fleet();
        
        if (ships != null) {
            f.ships = new Array<ShipStack>();
            for (shipStack in ships) {
                var stackCopy = { amount : shipStack.amount, 
                                  type : shipStack.type.clone(),
                                  armor : shipStack.armor,
                                  shields : shipStack.shields,
                                  battlePlan : shipStack.battlePlan.clone() };
                f.ships.push(stackCopy);
            }
        }
        
        f.name = name;
        f.owner = owner;
        f.fuel = fuel;
        f.colonists = colonists;
        f.minerals = minerals.clone();
        f.bearing = bearing;
        f.scannedMass = scannedMass;
        f.scannedWarp = scannedWarp;
        f.waypoint0 = waypoint0;
        
        if (waypoints != null) {
            f.waypoints = new Array<Waypoint>();
            
            for (waypoint in waypoints) {
                var waypointCopy = { warp : waypoint.warp,
                                     task : waypoint.task,
                                     target : waypoint.target };
                f.waypoints.push(waypointCopy);
            }
        }
        
        return f;
    }
}
